<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>工程参数</title>
  <link rel="stylesheet" href="/static/css/bootstrap.min.css">
  <link rel="stylesheet" href="/static/css/font-awesome.min.css">
  <link rel="stylesheet" href="/static/css/style.css">
  <link rel="stylesheet" href="/omsp/qv.css">
  <!--  <script src="/static/js/echarts.5.5.js"></script>-->
  <script src="/static/js/echarts.4.9.js"></script>

  <style>
    th, td {
      min-width: 100px !important;
      word-break: keep-all;
      white-space: pre
    }

    #c > div + div {
      overflow: hidden !important;
    }

    .E {
      color: red !important;
    }

    .table.small tr > * {
      padding: 3px 6px;
    }
    
    .loading-overlay {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(255, 255, 255, 0.8);
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 9999;
      font-size: 18px;
    }

    .loading-spinner {
      border: 4px solid #f3f3f3;
      border-top: 4px solid #3498db;
      border-radius: 50%;
      width: 40px;
      height: 40px;
      animation: spin 1s linear infinite;
      margin-right: 10px;
    }

    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }
  </style>
</head>
<body>
<h2>工程参数
  <small>
    <button class="btn btn-primary btn-sm " id="realtime" onclick="window.location.href='rt.html'">实时</button>
    <button class="btn btn-primary btn-sm mx-1 disactive-btn" id="history" disabled>历史</button>
    <button class="btn btn-success btn-sm mx-1" onclick="window.open('times.html', '_blank')">
      <i class="fa fa-clock-o"></i>时序历史
    </button>
  </small>
</h2>
<div id="CCC" style="padding: 1px 6px;display: flex;border-bottom: 1px solid #ccc">
  <label for="packages">科学载荷:</label>
  <select id="packages" style='width:200px;' onchange="dateChange()"></select>
  <label class="ps-3" for="sdate">日期:</label>
  <input id="sdate" type="date" value="2024-01-01" onchange="dateChange()"><label class="ps-3" for="task">任务:</label>
  <select id="task" class="sele-comp">
  </select>
  <button id="queryBtn" class="btn btn-primary btn-sm ms-2" onclick="manualQuery()">
    <i class="fa fa-search"></i>查询
  </button>
  <label for="frames" class="ps-3" id="frames_sidx">起始位置:</label>
  <input type="range" min="0" value="0" max="0" id="frames" style="width: 200px"><span> </span>
  <a class="btn btn-primary btn-sm"
     onclick="downloadWithLoading(this)"><i
    class="fa fa-cloud-download"></i>下载</a>
</div>

<!-- Loading overlay -->
<div id="loadingOverlay" class="loading-overlay" style="display: none;">
  <div class="loading-spinner"></div>
  <span>正在查询数据...</span>
</div>

<!-- Download loading overlay -->
<div id="downloadLoadingOverlay" class="loading-overlay" style="display: none;">
  <div class="loading-spinner"></div>
  <span>正在导出数据，请稍候...</span>
</div>
<!--<br>-->
<select id="params" size="20" multiple
        style="outline: none;border: 1px solid lightgrey;height: calc(100vh - 80px);width: 350px;"></select>
<div style="position: absolute;top: 80px;bottom: 380px;left: 350px;right: 0;overflow: auto;">
  <table id="result" class="table table-bordered table-hover small m-0">
    <thead></thead>
    <tbody></tbody>
  </table>
</div>
<div id="c"
     style="overflow: hidden;height: 380px;position: absolute;bottom: 0;right: 0;left: 350px;border-top: 1px solid lightgrey"></div>
<script src="/static/js/jquery-3.6.0.min.js"></script>
<script src="/static/js/xtool.js"></script>
<script src="../qv.js"></script>
<script src="index.js"></script>
<script>
  const local_offset = new Date().getTimezoneOffset() * 60e3
  let NLINE = 16;
  let $FMT = $$('#formatter')
  // 图多分类
  let CLIMT = 9
  let chart = echarts.init($$('#c'), null, {devicePixelRatio: 2, renderer: 'canvas'})
  option = {
    useUTC: true,
    color: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'],
    xAxis: {type: 'time'},
    yAxis: {type: 'value', scale: true},
    legend: {right: 50, top: 15},
    toolbox: {
      top: 15,
      right: 10,
      orient: 'vertical',
      feature: {saveAsImage: {type: 'png', pixelRatio: 5}, dataZoom: {yAxisIndex: false}}
    },
    grid: {top: 40, left: 80, right: 50, bottom: 70},
    tooltip: {
      trigger: 'axis', axisPointer: {type: 'line', axis: 'x'},
      position: (pos, params, el, elRect, size) => {
        chart.curDataIndex = params[0].dataIndex;
      },
      extraCssText: 'background-color: #fff9;color: #444;border: 1px solid #aaa'
    }, 
    animation: false,
    // 性能优化配置
    progressive: 5000,        // 渐进式渲染阈值
    progressiveThreshold: 10000,  // 渐进式渲染的数据量阈值
    hoverLayerThreshold: 3000,    // hover层阈值
    dataZoom: [
      {type: 'slider', height: 20, xAxisIndex: 0, show: true, filterMode: 'weakFilter'},
      {type: 'inside', xAxisIndex: 0, filterMode: 'weakFilter'}]
  }
  // chart.showLoading()
  // chart.setOption(option)
  let db = new Inflex()
  let data;
  $pkg = $('#packages');
  $prm = $('#params');
  $frm = $('#frames');
  $pgs = $('#frames+*');
  $TH = $('#result>thead');
  $TB = $('#result>tbody');
  $sdate = $('#sdate')
  $queryBtn = $('#queryBtn')
  $loadingOverlay = $('#loadingOverlay')
  $downloadLoadingOverlay = $('#downloadLoadingOverlay')
  $frm[0].step = NLINE;
  let $name = ''
  var _tfmt = (t) => t.toISOString().slice(0, 19).replace('T', ' ');

  // 显示loading
  function showLoading() {
    $loadingOverlay.show();
    $queryBtn.prop('disabled', true);
  }

  // 隐藏loading
  function hideLoading() {
    $loadingOverlay.hide();
    $queryBtn.prop('disabled', false);
  }

  // 显示下载loading
  function showDownloadLoading() {
    $downloadLoadingOverlay.show();
  }

  // 隐藏下载loading
  function hideDownloadLoading() {
    $downloadLoadingOverlay.hide();
  }

  // 下载包装函数
  async function downloadWithLoading(btn) {
    showDownloadLoading();
    try {
      // 调用原有的下载函数
      await current_packages.to_csv($prm.val(), btn, current_packages.keys, $name);
    } catch (error) {
      console.error('下载失败:', error);
      alert('下载失败，请重试');
    } finally {
      hideDownloadLoading();
    }
  }

  window.addEventListener('message', function (e) {
    console.log(e)
    if (e.data.includes('-')) {
      $sdate.val(e.data)
      console.log($sdate)
    }
    dateChange();
  });

  async function load_format(name) {
    return await import(`${window.location}`.split('/').slice(0, -1).join('/') + `/${name}.js`).then(moudle => moudle.params())
  }


  async function dateChange() {
    let sta = getStation();
    let pkg = $pkg.val()
    let theday = $sdate.val()
    let pa = `ins=DAT/${sta}&day=${theday}`
    ops = await fetch('/-/omsp.tasks?' + pa).then(v => v.json())
    $('#task').html('')
    $('#task').html(ops.map(op => {
      r = document.createElement('option')
      v = op.split('/')[5] + '/' + pkg
      console.log(v)
      r.value = op;
      r.innerHTML = `${v}`;
      return r;
    }))
    if (ops.length > 0) {
      // 确保第一个选项被选中
      $('#task')[0].selectedIndex = 0;
      // 移除自动查询，改为显示提示
      $TB.html(`<tr><td style="color: #999" colspan="10">请点击查询按钮获取数据</td></tr>`);
    }
    else {
      $TB.html(`<tr><td style="color: #999" colspan="10">当天无任务数据</td></tr>`);
      chart.clear();
    }
  }

  window.onload = async function () {
    $sdate.val(window.location.hash.slice(1));
    let u = await fetch('/-/auth.userinfo').then(v => v.json());
    let dic = {id: '0C0B', name: '载荷管理单元', jn: 'APMU'};
    q_ = await fetch('list.json').then(v => v.json());
    var q;
    if (u.role.includes('adm')) {
      q = q_
    } else {
      q = q_.filter(i => i.jn.includes(u.role))
        .map(i => i);
      q.push(dic);
    }

    $pkg.html(q.map(v => {
      r = document.createElement('option')
      r.value = v.jn;
      r.innerHTML = `${v.name} / ${v.jn}`;
      return r;
    }))
    pkg_change();
    
    // 启动性能监控
    startPerformanceMonitoring();
  }
  
  // 性能监控系统
  const performanceMonitor = {
    memoryWarningThreshold: 500 * 1024 * 1024, // 500MB
    lastMemoryCheck: 0,
    checkInterval: 5000, // 每5秒检查一次
    
    checkMemory() {
      if (performance.memory) {
        const used = performance.memory.usedJSHeapSize;
        const limit = performance.memory.jsHeapSizeLimit;
        const usedMB = Math.round(used / 1048576);
        const limitMB = Math.round(limit / 1048576);
        
        if (used > this.memoryWarningThreshold) {
          console.warn(`⚠️ 内存使用较高: ${usedMB}MB / ${limitMB}MB (${Math.round(used/limit*100)}%)`);
          
          // 如果内存使用超过80%，建议清理
          if (used / limit > 0.8) {
            this.suggestCleanup();
          }
        }
        
        return { used: usedMB, limit: limitMB, percentage: Math.round(used/limit*100) };
      }
      return null;
    },
    
    suggestCleanup() {
      if (Date.now() - this.lastMemoryCheck > 30000) { // 30秒内只提示一次
        this.lastMemoryCheck = Date.now();
        if (confirm('内存使用较高，可能影响性能。是否清理缓存并刷新页面？')) {
          // 清理数据
          if (current_packages) {
            current_packages.values = [];
          }
          if (chart) {
            chart.clear();
          }
          // 刷新页面
          location.reload();
        }
      }
    },
    
    measureRenderTime(fn, label) {
      const startTime = performance.now();
      const result = fn();
      const endTime = performance.now();
      const duration = endTime - startTime;
      
      if (duration > 100) { // 超过100ms认为是慢渲染
        console.log(`⏱️ ${label} 耗时: ${duration.toFixed(2)}ms`);
      }
      
      return result;
    },
    
    logDataStats() {
      if (current_packages && current_packages.values) {
        const dataCount = current_packages.values.length;
        const memory = this.checkMemory();
        console.log(`📊 数据统计: ${dataCount}条记录${current_packages.isSampled ? ' (已采样)' : ''}`);
        if (memory) {
          console.log(`💾 内存使用: ${memory.used}MB (${memory.percentage}%)`);
        }
      }
    }
  };
  
  // 启动性能监控
  function startPerformanceMonitoring() {
    // 定期检查内存
    setInterval(() => {
      performanceMonitor.checkMemory();
    }, performanceMonitor.checkInterval);
    
    // 监控页面可见性变化，优化后台性能
    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        // 页面隐藏时降低更新频率
        console.log('页面已隐藏，降低资源消耗');
      } else {
        // 页面显示时恢复
        console.log('页面已显示');
      }
    });
    
    // 监控窗口失焦，优化性能
    window.addEventListener('blur', () => {
      // 窗口失焦时可以降低渲染频率
      if (chart) {
        chart.setOption({ animation: false });
      }
    });
    
    window.addEventListener('focus', () => {
      // 窗口获得焦点时恢复
      if (chart) {
        chart.setOption({ animation: false }); // 保持动画关闭以优化性能
      }
    });
  }
  var current_packages

  let nmap = {
    "0C0B": "APMU", "0813": "AMAC", "0909": "ATES", "091A": "AVIRIS", "092C": "AMSC", "093F": "ACSR",
    "094F": "AMAG", "095C": "CANPA", "0A0A": "ADVA-VITA", "0A19": "ADVA-DMA"
  }
  let rmap = Object.keys(nmap).reduce((obj, key) => {
    obj[nmap[key]] = key;
    return obj;
  }, {})



  var keyAll = []

  async function pkg_change() {
    let pkg = $pkg.val().toLowerCase()
    $frm.val(0);
    
    // 切换载荷前清理旧数据
    if (current_packages && current_packages.values && current_packages.values.length > 0) {
      console.log('切换载荷，清理旧数据');
      current_packages.values = [];
      current_packages = null;
    }
    
    // 清理图表
    if (chart) {
      chart.clear();
    }
    
    prms = await load_format(pkg)
    $prm.html(prms.map(v => {
      r = document.createElement('option')
      r.data = v
      r.value = v.id;
      r.innerHTML = v.id + ' / ' + v.n;
      return r;
    }))
    console.log($prm.val());
    current_packages = new PKGS(prms);
    console.log(current_packages)
    current_packages.n = $pkg.val().toLowerCase();
    await dateChange();
    
    // 清理表格显示
    $TB.html(`<tr><td style="color: #999" colspan="10">请点击查询按钮获取数据</td></tr>`);
  }

  window.addEventListener('message', pkg_change);

  // 手动查询函数
  async function manualQuery() {
    if (!$('#task').val()) {
      alert('请先选择任务！');
      return;
    }

    showLoading();
    
    try {
      // 查询前清理旧数据，释放内存
      cleanupOldData();
      
      await taskChange();
      showtable();
      display_chart();
      
      // 查询后记录统计信息
      performanceMonitor.logDataStats();
    } catch (error) {
      console.error('查询失败:', error);
      alert(`查询失败: ${error.message}`);
    } finally {
      hideLoading();
    }
  }
  
  // 清理旧数据，释放内存
  function cleanupOldData() {
    // 清理旧的表格缓存
    if (tableRenderCache) {
      tableRenderCache.lastKeys = null;
      tableRenderCache.lastStart = -1;
      tableRenderCache.lastEnd = -1;
    }
    
    // 清理旧的包数据
    if (current_packages && current_packages.values && current_packages.values.length > 10000) {
      console.log('清理旧数据以释放内存');
      current_packages.values = [];
    }
    
    // 清理图表
    if (chart) {
      chart.clear();
    }
    
    // 强制垃圾回收（如果可用）
    if (window.gc) {
      window.gc();
    }
  }

  // 数据量预检查函数
  async function checkDataVolume(pkg, arc, track, sta) {
    try {
      let countData = await db.execute(
        `select count(time) as count
         from "tw2g${rmap[pkg]}a"
         where arc = '${arc}'
           and track = '${track}'
           and station = '${sta}'`
      );
      
      if (countData && countData.results && countData.results[0] && countData.results[0].series) {
        const count = countData.results[0].series[0].values[0][1];
        return count || 0;
      }
      return 0;
    } catch (error) {
      console.error('数据量检查失败:', error);
      return 0;
    }
  }

  async function taskChange() {
    let pkg = $pkg.val();
    let sta = getStation()
    let taskVal = $('#task').val();
    
    // 添加空值检查
    if (!taskVal) {
      console.warn('任务值为空，无法继续处理');
      return;
    }
    
    let t = taskVal.split('/')
    let tt = t[t.length - 2].split('_')
    let arc = tt[2]
    let track = tt[1]
    $name = `工程参数_${sta}_${pkg}_${$sdate.val()}_${track}_${arc}`

    // 获取选中的参数，确保包含time字段
    let selected = $prm.val();
    let columns = ['time'];
    if (selected && selected.length > 0) {
      columns = ['time', ...selected];
    } else {
      columns = ['*'];
    }

    // 数据量预检查
    const dataCount = await checkDataVolume(pkg, arc, track, sta);
    let sampleClause = '';
    let useDataSampling = false;
    
    // 设置采样阈值
    const SAMPLE_THRESHOLD = 50000;  // 超过5万条数据启用采样
    const DISPLAY_THRESHOLD = 10000; // 超过1万条数据提示用户
    
    if (dataCount > DISPLAY_THRESHOLD) {
      console.log(`数据量: ${dataCount} 条`);
      
      if (dataCount > SAMPLE_THRESHOLD) {
        // 计算采样率
        const sampleRate = Math.ceil(dataCount / 10000); // 目标采样到1万条左右
        const sampleInterval = `${sampleRate}s`;
        sampleClause = ` group by time(${sampleInterval}) fill(previous)`;
        useDataSampling = true;
        
        // 提示用户
        alert(`检测到大数据量（${dataCount}条），已自动启用智能采样（每${sampleRate}条取1条），确保页面流畅运行。\n如需完整数据，请缩小查询时间范围。`);
      } else {
        // 数据量较大但未达到采样阈值，提示用户
        console.log(`数据量较大（${dataCount}条），可能需要较长加载时间`);
      }
    }

    // 为字段名添加双引号分隔符，避免数字开头的字段名解析错误
    let quotedColumns = columns.map(col => col === '*' ? '*' : `"${col}"`);
    
    // 构建查询SQL，根据数据量决定是否采样
    let sql;
    if (useDataSampling && columns[0] !== '*') {
      // 使用采样查询（针对具体字段）
      const fieldSelections = quotedColumns.map(col => 
        col === '"time"' ? 'time' : `last(${col}) as ${col}`
      ).join(', ');
      
      sql = `select ${fieldSelections}
             from "tw2g${rmap[pkg]}a"
             where arc = '${arc}'
               and track = '${track}'
               and station = '${sta}'
             ${sampleClause}
             order by time`;
    } else {
      // 常规查询或全字段查询
      sql = `select ${quotedColumns.join(', ')}
             from "tw2g${rmap[pkg]}a"
             where arc = '${arc}'
               and track = '${track}'
               and station = '${sta}'
             order by time`;
    }
    
    console.log('执行查询:', sql);
    let data = await db.execute(sql);
    
    let pkgs = current_packages.dataset(data)
    
    // 如果启用了采样，在数据对象中标记
    if (useDataSampling && current_packages) {
      current_packages.isSampled = true;
      current_packages.originalCount = dataCount;
    }
    
    $frm[0].max = pkgs ? pkgs.length : 0;
    return pkgs
  }


  function display_chart() {
    chart.clear()
    let selected = $prm.val()
    let len = selected.length
    if (len > 0 && len < CLIMT) {
      chart.showLoading()
      
      // 获取数据
      let d = selected.map(k => {
        let seriesData = current_packages.data(k);
        
        // 检查数据量，如果过大则进行前端采样
        if (seriesData.data && seriesData.data.length > 5000) {
          console.log(`系列 ${seriesData.name} 数据量过大(${seriesData.data.length})，进行前端采样优化`);
          seriesData = sampleSeriesData(seriesData, 5000);
        }
        
        // 添加大数据优化配置
        seriesData.large = true;           // 启用大数据模式
        seriesData.largeThreshold = 2000;  // 大数据模式阈值
        seriesData.sampling = 'lttb';      // 使用LTTB采样算法
        seriesData.progressiveChunkMode = 'sequential';  // 渐进式渲染模式
        
        return seriesData;
      });
      
      let names = d.map(v => v.name)
      
      // 设置图表配置
      chart.setOption(option)
      
      // 添加采样提示
      let titleText = current_packages.n;
      if (current_packages.isSampled) {
        titleText += ` (已采样: ${current_packages.values.length}/${current_packages.originalCount}条)`;
      }
      
      chart.setOption({
        series: d, 
        title: {text: titleText}, 
        legend: {data: names}
      })
      
      chart.hideLoading()
    }
  }
  
  // 数据采样函数 - 使用最大三角形三桶算法(LTTB)
  function sampleSeriesData(seriesData, targetCount) {
    const data = seriesData.data;
    if (!data || data.length <= targetCount) {
      return seriesData;
    }
    
    const sampled = [];
    const bucketSize = Math.floor(data.length / targetCount);
    
    // 保留第一个点
    sampled.push(data[0]);
    
    for (let i = 0; i < targetCount - 2; i++) {
      const bucketStart = (i + 1) * bucketSize;
      const bucketEnd = Math.min(bucketStart + bucketSize, data.length - 1);
      
      // 找到桶内最有代表性的点
      let maxArea = 0;
      let maxAreaIndex = bucketStart;
      
      for (let j = bucketStart; j < bucketEnd; j++) {
        // 计算三角形面积
        const area = Math.abs(
          (data[j][0] - sampled[sampled.length - 1][0]) * (data[bucketEnd][1] - sampled[sampled.length - 1][1]) -
          (data[j][1] - sampled[sampled.length - 1][1]) * (data[bucketEnd][0] - sampled[sampled.length - 1][0])
        );
        
        if (area > maxArea) {
          maxArea = area;
          maxAreaIndex = j;
        }
      }
      
      sampled.push(data[maxAreaIndex]);
    }
    
    // 保留最后一个点
    sampled.push(data[data.length - 1]);
    
    return {
      ...seriesData,
      data: sampled,
      _originalLength: data.length  // 记录原始数据长度
    };
  }

  $pkg.change(pkg_change)
  $prm.change(() => {
    // 参数选择变化时不自动查询，只清空显示
    $TB.html(`<tr><td style="color: #999" colspan="10">请点击查询按钮获取数据</td></tr>`)
    chart.clear()
  })

  // 表格虚拟滚动优化
  let tableRenderCache = {
    lastKeys: null,
    lastStart: -1,
    lastEnd: -1
  };
  
  function showtable(keys, pkgs) {
    let selected = $prm.val()
    keys = keys || (selected.length > 0 ? selected : current_packages.keys);
    pkgs = pkgs || current_packages.values;
    let s = Number($frm.val());
    
    // 设置表头（只在keys改变时更新）
    const keysChanged = JSON.stringify(keys) !== JSON.stringify(tableRenderCache.lastKeys);
    if (keysChanged) {
      $TH.html(current_packages.thead(keys));
      tableRenderCache.lastKeys = keys;
    }
    
    if (pkgs && pkgs.length > 0) {
      const endIdx = Math.min(s + NLINE, pkgs.length);
      
      // 检查是否需要重新渲染
      if (s === tableRenderCache.lastStart && endIdx === tableRenderCache.lastEnd && !keysChanged) {
        return; // 无需重新渲染
      }
      
      // 使用requestAnimationFrame优化渲染
      requestAnimationFrame(() => {
        // 使用文档片段优化DOM操作
        const fragment = document.createDocumentFragment();
        const tbody = document.createElement('tbody');
        
        // 批量生成行HTML
        const rowsHtml = [];
        for (let i = s; i < endIdx; i++) {
          rowsHtml.push(pkgs[i].tr(keys));
        }
        
        // 一次性设置HTML，减少DOM操作
        tbody.innerHTML = rowsHtml.join('');
        
        // 替换整个tbody内容
        $TB[0].innerHTML = tbody.innerHTML;
        
        $pgs.html(`${s + 1}-${endIdx}/${pkgs.length}`);
        
        // 更新缓存
        tableRenderCache.lastStart = s;
        tableRenderCache.lastEnd = endIdx;
      });
    } else {
      $pgs.html(`0/0`);
      $frm[0].max = 0;
      $TB.html(`
<tr>
  <td style="color: #F00" colspan="${keys.length + 1}"> 无数据</td>
</tr>
`)
      tableRenderCache.lastStart = -1;
      tableRenderCache.lastEnd = -1;
    }
  }
  
  // 优化滚动性能 - 添加防抖
  let scrollDebounceTimer = null;
  function debounceShowTable() {
    if (scrollDebounceTimer) {
      clearTimeout(scrollDebounceTimer);
    }
    scrollDebounceTimer = setTimeout(() => {
      showtable();
    }, 50);
  }

  $frm.change(function () {
// showtable(keys, pkgs)
// console.log("frm.change.keys:",keys, pkgs)
    showtable()
  });
  window.onresize = chart.resize;
  chart.getDom().onclick = () => {
    $frm.val(Math.floor(chart.curDataIndex / NLINE) * NLINE);
    showtable();
  }
  chart.on('click', function (e) {
    $frm.val(e.dataIndex);
    showtable()
  })

  function save() {
    a = document.createElement('a');
    a.href = chart.getDataURL({type: 'png', pixelRatio: 1});
    a.download = current_packages.name + '.png';
    a.click();
  }

  function turnPage(nextFlag) {
    let val = Number($frm.val())
    $frm.val(val + (nextFlag ? +NLINE : -NLINE));
    debounceShowTable(); // 使用防抖版本
  }

  // 优化鼠标滚轮事件处理
  let wheelDebounceTimer = null;
  $TB[0].onmousewheel = e => {
    if (e.shiftKey || Number($frm[0].step) >= $frm[0].max || (Number($frm.val()) + Number($frm[0].step) >= $frm[0].max && e.wheelDelta < 0)) return
    
    // 使用防抖处理滚轮事件
    if (wheelDebounceTimer) {
      clearTimeout(wheelDebounceTimer);
    }
    wheelDebounceTimer = setTimeout(() => {
      turnPage(e.wheelDelta < 0);
    }, 30);
  };
</script>
</body>
</html>
